require 'set'
require 'jinx/helpers/inflector'
require 'jinx/helpers/validation'

module CaTissue
  class AbstractSpecimen
    # Sets the specimen type to the specified value. The value can be a permissible caTissue String value or
    # the shortcut symbols :fresh, :fixed and +:frozen+.
    #
    # @param [String, Symbol, nil] value the value to set
    def specimen_type=(value)
      value = value.to_s.capitalize_first + ' Tissue' if Symbol === value
      setSpecimenType(value)
    end

    add_attribute_aliases(:parent => :parent_specimen,
     :children => :child_specimens,
     :events => :specimen_events,
     :specimen_event_parameters => :specimen_events,
     :event_parameters => :specimen_events,
     :characteristics => :specimen_characteristics)

    # @quirk caTissue initial_quantity must be set (cf. Bug #160)
    add_attribute_defaults(:initial_quantity => 0.0, :pathological_status => 'Not Specified', :specimen_type => 'Not Specified')

    add_mandatory_attributes(:lineage, :pathological_status, :specimen_class, :specimen_type, :specimen_characteristics)

    # @quirk caTissue Specimen characteristics are auto-generated but SpecimenRequirement
    #   characteristics are not. It is safe to set the +:autogenerated+ flag for both
    #   AbstractSpecimen subclasses. This results in an unnecessary SpecimenRequirement
    #   create database query, but SpecimenRequirement create is rare and there is no harm.
    #
    # @quirk caTissue Specimen characteristics is cascaded but is not an exclusive dependent,
    #   since it is shared by aliquots.
    #
    # @quirk caTissue Bug 166: API update Specimen ignores a SpecimenCharacteristics with a
    #   different id. Guard against updating a Specimen with a SpecimenCharacteristics whose id
    #   differs from the existing id.
    #
    # @quirk caTissue Unlike other dependents, AbstractSpecimen characteristics, events and child
    #   specimens have cascade style +all+. This implies that an AbstractSpecimen update does not
    #   create a referenced dependent. AbstractSpecimen create cascades to create and AbstractSpecimen
    #   update cascades to update, but AbstractSpecimen update does not cascade to create.
    #   The +:no_cascade_update_to_create flag+ is set to handle this feature of cascade style +all+.
    qualify_attribute(:specimen_characteristics, :cascaded, :fetched, :autogenerated, :no_cascade_update_to_create)

    # The +:no_cascade_update_to_create+ flag is set since specimen_events has cascade style +all+.
    add_dependent_attribute(:specimen_event_parameters, :disjoint)
    
    set_attribute_inverse(:parent_specimen, :child_specimens)
    
    # @quirk caTissue A Specimen update cascades to a child specimen, but the update is not reflected
    #   in the caTissue updateObject result. Work-around is to fetch the updated child from the
    #   database to ensure that the database content reflects the intended update argument.
    #
    # The +:no_cascade_update_to_create+ flag is set since child_specimens has cascade style +all+.
    add_dependent_attribute(:child_specimens, :unfetched, :fetch_saved, :no_cascade_update_to_create)

    # The specimen_class attribute constants.
    class SpecimenClass
      TISSUE = 'Tissue'
      FLUID = 'Fluid'
      MOLECULAR = 'Molecular'
      CELL = 'Cell'

      # The standard units used for each specimen class.
      UNIT_HASH = {TISSUE => 'gm', FLUID => 'ml', MOLECULAR => 'ug'}

      # @return [Boolean] whether the value is an accepted tissue class value
      def self.include?(value)
        EXTENT.include?(value)
      end
      
      private

      EXTENT = Set.new([TISSUE, FLUID, MOLECULAR, CELL])
    end

    # Initializes this AbstractSpecimen. The default +specimen_class+ is inferred from this
    # AbstractSpecimen instance's subclass.
    def initialize
      super
      self.specimen_class ||= infer_specimen_class(self.class)
    end

    # @return [Boolean] whether this specimen is derived from a parent specimen
    def derived?
      !!parent
    end

    # @return [Boolean] whether this specimen is an aliquot
    def aliquot?
      lineage ||= default_lineage
      lineage == 'Aliquot'
    end

    # @return [<AbstractSpecimen>] this specimen's aliquots
    def aliquots
      children.filter { |child| child.aliquot? }
    end

    # @return [Boolean] whether this specimen's type is 'Fresh Tissue'
    def fresh?
      specimen_type == 'Fresh Tissue'
    end

    # @return [Boolean] whether this specimen's type starts with 'Fixed'
    def fixed?
      specimen_type =~ /^Fixed/
    end

    # @return [Boolean] whether this specimen's type starts with 'Frozen'
    def frozen?
      specimen_type =~ /^Frozen/
    end

    # @return <AbstractSpecimen> the transitive closure consisting of this AbstractSpecimen
    # and all AbstractSpecimen in the derivation hierarcy.
    def closure
      children.inject([self]) { |coll, spc| coll.concat(spc.closure) }
    end

    # Returns the standard unit for this specimen
    def standard_unit
      self.specimen_class ||= infer_specimen_class(self.class)
      SpecimenClass::UNIT_HASH[self.specimen_class]
    end

    # Derives a new specimen from this specimen. The options are described in
    # {Specimen.create_specimen}, with one addition:
    # * +:count+(+Integer+) - the optional number of specimens to derive
    #
    # If the +:count+ option is greater than one and the +:specimen_class+,
    # +:specimen_type+ and +:specimen_characteristics+ options are not set to values
    # which differ from the respective values for this Specimen, then the specimen is
    # aliquoted, otherwise the derived specimens are created independently, e.g.:
    #   spc = Specimen.create_specimen(:specimen_class => :tissue, :specimen_type => :frozen)
    #   spc.derive(:count => 1) #=> not aliquoted
    #   spc.derive(:count => 2) #=> aliquoted
    #   spc.derive(:specimen_type => 'Frozen Specimen') #=> two aliquots
    #
    # The default derived _initial_quantity_ is the parent specimen _available_quantity_
    # divided by _count_ for aliquots, zero otherwise. If the child _specimen_class_
    # is the same as this Specimen class, then this parent Specimen's _available_quantity_
    # is decremented by the child _initial_quantity_, e.g.:
    #   spc.available_quantity #=> 4
    #   spc.derive(:initial_quantity => 1)
    #   spc.available_quantity #=> 3
    #   spc.derive(:count => 2, :specimen_type => 'Frozen Tissue')
    #   spc.derive(:count => 2) #=> two aliquots with quantity 1 each
    #   spc.available_quantity #=> 0
    #
    # The default derived specimen label is _label_+_+_n_, where _label_ is this specimen's
    # label and _n_ is this specimen's child count after including the new derived specimen,
    # e.g. +3090_3+ for the third child in the parent specimen with label +3090+.
    #
    # @param [{Symbol => Object}, nil] opts the attribute => value hash
    # @return [AbstractSpecimen, <AbstractSpecimen>] the new derived specimen if the +:count+ option
    #   is missing or one, otherwise an Array of _count_ derived specimens
    # @raise [Jinx::ValidationError] if an aliquoted parent available quantity is not greater than zero
    #   or the derived specimen quantities exceed the parent available quantity
    def derive(opts=Hash::EMPTY_HASH)
      # add defaults
      add_defaults if specimen_class.nil?
      # copy the option hash
      opts = opts.dup
      # standardize the requirement param, if any
      rqmt = opts.delete(:requirement)
      opts[:specimen_requirement] ||= rqmt if rqmt
      # the default specimen parameters
      unless opts.has_key?(:specimen_requirement) then
        opts[:specimen_class] ||= self.specimen_class ||= infer_specimen_class
        opts[:specimen_type] ||= self.specimen_type
      end
      unless Class === opts[:specimen_class] then
        opts[:specimen_class] = infer_class(opts)
      end
      count = opts.delete(:count)
      count ||= 1
      aliquot_flag = false
      if count > 1 and opts[:specimen_class] == self.class and opts[:specimen_type] == self.specimen_type then
        # aliquots share the specimen_characteristics
        child_chr = opts[:specimen_characteristics] ||= specimen_characteristics
        aliquot_flag = child_chr == specimen_characteristics
      end
      # set aliquot parameters if necessary
      if aliquot_flag then set_aliquot_parameters(opts, count) end
      # make the derived specimens
      count == 1 ? create_derived(opts) : Array.new(count) { create_derived(opts) }
    end

    # Returns whether this AbstractSpecimen is minimally consistent with the other specimen.
    # This method augments the +Jinx::Resource.minimal_match?+ with an additional restriction
    # that the other specimen is the same type as this specimen and
    # is a tolerant match on specimen class, specimen type and pathological status.
    # A _tolerant_ match condition holds if the other attribute value is equal to this
    # AbstractSpecimen's attribute value or the other value is the default 'Not Specified'.
    #
    # @param (see Jinx::Resource#minimal_match?)
    # @return (see Jinx::Resource#minimal_match?)
    def minimal_match?(other)
      super and tolerant_match?(other, TOLERANT_MATCH_ATTRS)
    end
    
    private
    
    TOLERANT_MATCH_ATTRS = [:specimen_class, :specimen_type, :pathological_status]

    # The attributes which can be merged as defaults from a parent into a derived child Specimen.
    DERIVED_MERGEABLE_ATTRS = [:activity_status, :pathological_status, :specimen_class, :specimen_type]

    # Sets special aliquot parameters for the given count of aliquots.
    # This default implementation is a no-op. Subclasses can override.
    #
    # @param [{Symbol => Object}] params the specimen attribute => value hash
    # @param [Integer] count the number of aliquots
    def set_aliquot_parameters(params, count); end

    # Overrides +Jinx::Resource.each_defaultable_reference} to visit the {CaTissue::SpecimenCharacteristics+.
    # The characteristics are not dependent since they can be shared among aliquots.
    # However, the defaults should be added to them. Do so here.
    #
    # @yield (see Jinx::Resource#each_defaultable_reference)
    def each_defaultable_reference
      super { |dep| yield dep }
      yield characteristics if characteristics
    end
 
    def add_defaults_local
      super
      # parent pathological status is preferred over the configuration defaults
      self.pathological_status ||= parent.pathological_status if parent
      # the configuration defaults
      # set the required but redundant tissue class and lineage values
      self.specimen_class ||= infer_specimen_class(self.class)
      self.lineage ||= default_lineage
      # copy the parent characteristics or add empty characteristics
      self.characteristics ||= default_characteristics
    end

    # Returns the Class from the given params hash.If the +:specimen_class+ parameter
    # is set to a Class, then this method returns that Class. Otherwise, if the parameter is a
    # String or Symbol, then the Class is formed from the parameter as a prefix and 'Specimen' or
    # 'SpecimenRequirement' depending on this AbstractSpecimen's subclass. If the
    # :specimen_class parameter is missing and there is a +:specimen_requirement+ parameter,
    # then the specimen requirement specimen_class attribute value is used.
    #
    # @param [{Symbol => Object}] params the specimen attribute => value hash
    # @return [Class] the AbstactSpecimen subclass to use
    def infer_class(params)
      opt = params[:specimen_class]
      if opt.nil? then
        rqmt = params[:specimen_requirement]
        opt = rqmt.specimen_class if rqmt
      end
      raise ArgumentError.new("Specimen class is missing from the create parameters") if opt.nil?
      return opt if Class === opt
      # infer the specimen domain class from the specimen_class prefix and Specimen or SpecimenRequirement suffix
      cls_nm = opt.to_s.capitalize_first + 'Specimen'
      cls_nm += 'Requirement' if CaTissue::SpecimenRequirement === self
      CaTissue.const_get(cls_nm)
    end

    # Creates a derived specimen. The options is an attribute => value hash. The options
    # can also include a +:specimen_requirement+. The non-domain attribute values of the
    # new derived specimen are determined according to the following precedence rule:
    # 1. The attribute => value option.
    # 2. The requirement property value.
    # 3. This specimen's property value.
    #
    # @param [{Symbol => Object}] opts the derived specimen attribute => value hash
    # @return [AbstractSpecimen] the derived specimen or requirement
    def create_derived(opts)
      # Merge the non-domain attribute values from this specimen, unless there is a requirement.
      opts = value_hash(DERIVED_MERGEABLE_ATTRS).merge!(opts) unless opts.has_key?(:specimen_requirement)
      # Copy this specimen's characteristics, if not already given in the options.
      opts[:specimen_characteristics] ||= default_derived_characteristics
      # Make the new specimen.
      spc = Specimen.create_specimen(opts)
      # The derived specimen's parent is this specimen.
      spc.parent = self
      spc
    end

    # Returns characteristics to use for a derived specimen. The new characteristics is copied from this
    # parent specimen's characteristics, without the identifier.
    #
    # @return [CaTissue::SpecimenCharacteristics, nil] a copy of this Specimen's specimen_characteristics, or nil if none
     def default_derived_characteristics
      chrs = specimen_characteristics || return
      pas = chrs.class.nondomain_attributes.reject { |pa| pa == :identifier }
      chrs.copy(pas)
    end

    # @return a copy of the parent characteristics, if any, or a new SpecimenCharacteristics otherwise
    def default_characteristics
      if parent and parent.characteristics then
        parent.characteristics.copy
      else
        CaTissue::SpecimenCharacteristics.new
      end
    end

    # Returns the the default lineage value computed as follows:
    # * if there is no parent specimen, then 'New'
    # * otherwise, if this Specimen's specimen_characteristics object is identical to that of
    #   the parent, then 'Aliquot'
    # * otherwise, 'Derived'
    # The aliquot condition requires that the specimen_characteristics is the same object,
    # not just the same content. This odd condition is a caTissue artifact. A more sensible
    # criterion is whether this Specimen and its parent share the same tissue_class and
    # tissue_type, but so it goes.
    #
    # @return ['New', 'Aliqout', 'Derived'] the lineage to use
    def default_lineage
      if parent.nil? then
        'New'
      elsif specimen_characteristics.equal?(parent.specimen_characteristics) then
        'Aliquot'
      else
        'Derived'
      end
    end

    # Infers the specimen class from the first word on the specified klass Ruby class name.
    #
    # @example
    #   infer_specimen_class(CaTissue::TissueRequirement) #=> "Tissue"
    #
    # @param [Class, nil] the AbstractSpecimen domain object class (default this specimen's class)
    # @return [String] the +specimen_class+ value
    def infer_specimen_class(klass=self.class)
      klass.to_s[/(\w+?)(Specimen(Requirement)?)$/, 1] if klass
    end
  end
end